/*
    This file is part of QTau
    Copyright (C) 2013-2018  Tobias "Tomoko" Platen <tplaten@posteo.de>
    Copyright (C) 2013       digited       <https://github.com/digited>
    Copyright (C) 2010-2013  HAL@ShurabaP  <https://github.com/haruneko>

    QTau is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    SPDX-License-Identifier: GPL-3.0+
*/


#include "Session.h"
#include "Utils.h"

#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <QStringList>

#include <QtAlgorithms>
#include <ustjkeys.h>

#include <QJsonDocument>
#include <math.h>
#include "ustfile.h"



#include <iostream>
#include "sinsyscoreconverter.h"
#include "midifile.h"


#define __devloglevel__ 4

qtauSession::qtauSession(QObject *parent) :
    qtauEventManager(parent), _docName(QStringLiteral("Untitled")), _isModified(false), _hadSavePoint(false)
{
    _defaults[USER_AGENT] = QString("QTau Debug");
    _defaults[TEMPO]=120;
    QJsonArray ts;
    ts.append(4);
    ts.append(4);
    _defaults[TIME_SIGNATURE]=ts;
    _objectMap[-1] = _defaults;
}

qtauSession::~qtauSession()
{
}

qtauEvent_NoteAddition *util_makeAddNotesEvent(QJsonArray &a)
{
    qtauEvent_NoteAddition::noteAddVector changeset;

    for (int i = 0; i < a.count(); ++i)
    {
        auto o = a[i];

        qtauEvent_NoteAddition::noteAddData d;
        d.id     = i+1;
        d.lyrics = o.toObject()[NOTE_LYRIC].toString();

        d.pulseLength = o.toObject()[NOTE_PULSE_LENGTH].toInt();
        d.pulseOffset = o.toObject()[NOTE_PULSE_OFFSET].toInt();
        d.keyNumber   = o.toObject()[NOTE_KEY_NUMBER].toInt();
        //d.velocity    = 100;//FIXME

        if(d.pulseLength==0 && d.pulseOffset==0)
        {
            //qDebug() << "dummy";
            continue;
        }

        QStringList keys;
        keys << XML_ACCENT;
        keys << XML_STACATTO;
        keys << XML_TIETYPE;
        keys << XML_SLURTYPE;
        keys << XML_SYLLABICTYPE;
        keys << XML_BREATH;
        foreach(QString key, keys)
        {
            if(o.toObject().contains(key))
                d.optionalElements[key] = o.toObject()[key];
        }
        //accent: ?
        //stacatto: use different adrs, time alignment
        //tietype: connect notes
        //slurtype: portamento handle, gsl for interpolation
        //syllabic, breath: not implemented yet




        changeset.append(d);
    }

    return new qtauEvent_NoteAddition(changeset);
}



bool qtauSession::loadUST(QJsonArray array)
{
    if (!array.isEmpty())
    {
        clearHistory();

        auto o = array[0];
        if(o.toObject().contains(USER_AGENT))
            _objectMap[-1] = o.toObject();
        else
            _objectMap[-1] = _defaults;


        qtauEvent_NoteAddition *loadNotesChangeset = util_makeAddNotesEvent(array);
        applyEvent_NoteAdded(*loadNotesChangeset);
        emit onEvent(loadNotesChangeset);
        delete loadNotesChangeset;
        return true;
    }
    return false;
}


bool qtauSession::loadUST(QString fileName)
{
    bool result = false;
    QFile ustFile(fileName);

    if (ustFile.open(QFile::ReadOnly))
    {
        QJsonDocument doc = QJsonDocument::fromJson(ustFile.readAll());
        if(doc.isArray())
        {
            // qDebug() << "lock";
            _docName  = QFileInfo(fileName).baseName();
            _filePath = fileName;
            emit dataReloaded();
            result = loadUST(doc.array());
            if(result==false) DEVLOG_ERROR("ust failed to load");
        }
        else
            DEVLOG_ERROR("json document is not an array");
        ustFile.close();
    }
    else
        DEVLOG_ERROR("Could not open " + fileName);

    return result;
}



inline void qSwap(QJsonValueRef var1, QJsonValueRef var2)
{
    QJsonValue tmp = var2;
    var2 = var1;
    var1 = tmp;
}


inline bool noteNumComparison(const QJsonValueRef o1, const QJsonValueRef& o2)
{
    return o1.toObject()[NOTE_KEY_NUMBER].toInt() < o2.toObject()[NOTE_KEY_NUMBER].toInt();
}

inline bool pulseOffsetComparison(const QJsonValueRef o1, const QJsonValueRef& o2)
{
    return o1.toObject()[NOTE_PULSE_OFFSET].toInt() < o2.toObject()[NOTE_PULSE_OFFSET].toInt();
}

void qtauSession::importUST(QString fileName)
{
    USTFile ust;
    ust.readFromFile(fileName);
    loadUST(ust.json());
}



void  qtauSession::ustJson(QJsonArray& ret)
{

    foreach (const quint64 &key, _objectMap.keys())
        ret.append(_objectMap[key]);

    qStableSort(ret.begin(), ret.end(), noteNumComparison);
    qStableSort(ret.begin(), ret.end(), pulseOffsetComparison);
}

void qtauSession::setDocName(const QString &name)
{
    if (name.isEmpty())
    {
        DEVLOG_ERROR("Shouldn't set empty doc name for session! Ignoring...")
    }
    else
        _docName = name;
}

void qtauSession::setFilePath(const QString &fp)
{
    if (fp.isEmpty())
    {
        DEVLOG_ERROR("Shouldn't set empty filepath for session! Ignoring...")
    }
    else
    {
        _filePath = fp;
        _docName = QFileInfo(fp).baseName();
    }
}

//----- inner data functions -----------------------------
void qtauSession::applyEvent_NoteAdded(const qtauEvent_NoteAddition &event)
{
    const qtauEvent_NoteAddition::noteAddVector &changeset = event.getAdded();

    // delete event has reversed transformations
    bool reallyForward = (event.isForward() && !event.isDeleteEvent()) ||
            (!event.isForward() &&  event.isDeleteEvent());

    if (reallyForward)
    {
        foreach (const qtauEvent_NoteAddition::noteAddData &change, changeset)
        {
            //noteMap[change.id] = ust_note(change.id, change.lyrics, change.pulseOffset, change.pulseLength, change.keyNumber);
            QJsonObject object;
            object[NOTE_LYRIC] = change.lyrics;
            object[NOTE_PULSE_OFFSET] = change.pulseOffset;
            object[NOTE_PULSE_LENGTH] = change.pulseLength;
            object[NOTE_KEY_NUMBER] = change.keyNumber;
            foreach(QString key, change.optionalElements.keys())
            {
                object[key] = change.optionalElements[key];
            }

            _objectMap[change.id] = object;
        }
    }
    else
        foreach (const qtauEvent_NoteAddition::noteAddData &change, changeset)
            _objectMap.remove(change.id);
}


void qtauSession::applyEvent_NoteResized(const qtauEvent_NoteResize &event)
{

    const qtauEvent_NoteResize::noteResizeVector &changeset = event.getResized();

    foreach (const qtauEvent_NoteResize::noteResizeData &change, changeset)
    {
        auto &n = _objectMap[change.id];

        if (event.isForward())
        {
            n[NOTE_PULSE_OFFSET] = change.offset;
            n[NOTE_PULSE_LENGTH] = change.length;
        }
        else
        {
            n[NOTE_PULSE_OFFSET] = change.prevOffset;
            n[NOTE_PULSE_LENGTH] = change.prevLength;
        }
    }

}


void qtauSession::applyEvent_NoteMoved(const qtauEvent_NoteMove &event)
{
    const qtauEvent_NoteMove::noteMoveVector &changeset = event.getMoved();

    foreach (const qtauEvent_NoteMove::noteMoveData &change, changeset)
    {
        auto &n = _objectMap[change.id];

        if (event.isForward())
        {
            n[NOTE_PULSE_OFFSET] = n[NOTE_PULSE_OFFSET].toInt() + change.pulseOffDelta;
            n[NOTE_KEY_NUMBER]   =  change.keyNumber;
        }
        else
        {
            n[NOTE_PULSE_OFFSET] = n[NOTE_PULSE_OFFSET].toInt() - change.pulseOffDelta;
            n[NOTE_KEY_NUMBER]   =  change.prevKeyNumber;
        }
    }
}


void qtauSession::applyEvent_NoteLyrics(const qtauEvent_NoteText &event)
{

    const qtauEvent_NoteText::noteTextVector &changeset = event.getText();

    foreach (const qtauEvent_NoteText::noteTextData &change, changeset)
        if (event.isForward()) _objectMap[change.id][NOTE_LYRIC] = change.txt;
        else                   _objectMap[change.id][NOTE_LYRIC] = change.prevTxt;
}


void qtauSession::applyEvent_NoteEffects(const qtauEvent_NoteEffect &/*event*/)
{
    //FIXME: to do or not to do, that is the question
    //implement other undoable UTAU things
}


//--------- dispatcher -----------------------------
void qtauSession::onUIEvent(qtauEvent *e)
{
    if (e)
    {
        if (processEvent(e))
            storeEvent(e);

        delete e; // if it's valid it was copied on storing, and UI should only create events anyway.
    }
}

// process event is called from both program (ui input) and undo/redo in manager (stack change)
bool qtauSession::processEvent(qtauEvent *e)
{
    bool result = false;

    if (e)
    {
        switch (e->type())
        {
        case ENoteEvents::add:
        {
            qtauEvent_NoteAddition *ne = static_cast<qtauEvent_NoteAddition*>(e);

            if (ne)
            {
                applyEvent_NoteAdded(*ne);
                result = true;
            }
            else
                DEVLOG_ERROR("Session could not convert UI event to noteAdd");

            break;
        }
        case ENoteEvents::move:
        {
            qtauEvent_NoteMove *ne = static_cast<qtauEvent_NoteMove*>(e);

            if (ne)
            {
                applyEvent_NoteMoved(*ne);
                result = true;
            }
            else
                DEVLOG_ERROR("Session could not convert UI event to noteMove");

            break;
        }
        case ENoteEvents::resize:
        {
            qtauEvent_NoteResize *ne = static_cast<qtauEvent_NoteResize*>(e);

            if (ne)
            {
                applyEvent_NoteResized(*ne);
                result = true;
            }
            else
                DEVLOG_ERROR("Session could not convert UI event to noteResize");

            break;
        }
        case ENoteEvents::text:
        {
            qtauEvent_NoteText *ne = static_cast<qtauEvent_NoteText*>(e);

            if (ne)
            {
                applyEvent_NoteLyrics(*ne);
                result = true;
            }
            else
             {
                DEVLOG_ERROR("Session could not convert UI event to noteText");
            }

            break;
        }
        case ENoteEvents::effect:
        {
            qtauEvent_NoteEffect *ne = static_cast<qtauEvent_NoteEffect*>(e);

            if (ne)
            {
                applyEvent_NoteEffects(*ne);
                result = true;
            }
            else
                DEVLOG_ERROR("Session could not convert UI event to noteEffect");

            break;
        }
        default:
            DEVLOG_ERROR(QString("Session received unknown event type from UI").arg(e->type()));
        }
    }
    else DEVLOG_ERROR("Session can't process a zero event! Ignoring...");

    return result;

}

void qtauSession::stackChanged()
{
    if (canUndo())
        _isModified = !events.top()->isSavePoint();
    else
        _isModified = _hadSavePoint;

    emit undoStatus(canUndo());
    emit redoStatus(canRedo());
    emit modifiedStatus(_isModified);
}

//Ardour is used to manage SynthesizedVocal and BackgroundAudio

void qtauSession::setModified(bool m)
{
    if (m != _isModified)
    {
        _isModified = m;
        emit modifiedStatus(_isModified);
    }
}

void qtauSession::setSaved()
{
    if (canUndo())
    {
        foreach (qtauEvent *e, events)
            e->setSavePoint(false);

        if (!futureEvents.isEmpty())
            foreach (qtauEvent *e, futureEvents)
                e->setSavePoint(false);

        _hadSavePoint = true;
        events.top()->setSavePoint();
        setModified(false);
    }
    else
        DEVLOG_ERROR("Saving an empty session?");
}

void qtauSession::importMIDI(QString fileName)
{
    QJsonArray ust;
    MidiFile f;
    f.loadMidi(fileName,ust);
    loadUST(ust);
}

void qtauSession::exportMIDI(QString fileName)
{
    QJsonArray ust;
    ustJson(ust);
    MidiFile f;
    f.saveMidi(ust,fileName);
}

void qtauSession::importMusicXML(QString fileName)
{
    sinsy::Sinsy sinsy;
    std::string xml = fileName.toStdString();

    if (!sinsy.loadScoreFromMusicXML(xml)) {
       std::cout << "[ERROR] failed to load score from MusicXML file : " << xml << std::endl;
       return ;
    }

    DEVLOG_DEBUG("valid musicxml");
    SinsyScoreConverter score;
    sinsy.toScore(score);
    loadUST(score.toUST());
}


//FIXME: connect directly to controller

void qtauSession::onNewSession()
{

    QJsonArray empty;

    clearHistory();
    _objectMap.clear();
    _objectMap[-1]          = _defaults;


    _docName  = "Untitled";
    _filePath = "";
    qtauEvent_NoteAddition *loadNotesChangeset = util_makeAddNotesEvent(empty);
    applyEvent_NoteAdded(*loadNotesChangeset);

    emit dataReloaded();//                  UPDATE text
    emit onEvent(loadNotesChangeset);       //LOAD SCORE INTO EDITOR

    delete loadNotesChangeset;
}

// set/get proptery

void qtauSession::setSingerName(QString singerName)
{
    //qDebug() << "setSingerName";
    QJsonObject obj;
    if(_objectMap.contains(-1)) obj=_objectMap[-1];
    obj[SINGER_NAME] = singerName;
    _objectMap[-1] = obj;
    _isModified = true;
    emit modifiedStatus(_isModified);
}

QString qtauSession::getSingerName()
{
    QJsonObject obj;
    if(_objectMap.contains(-1)) obj=_objectMap[-1];
    return obj[SINGER_NAME].toString();
}

void qtauSession::setTempoMap(QJsonArray tmap)
{
     QJsonObject obj;
     if(_objectMap.contains(-1)) obj=_objectMap[-1];
      obj[TEMPOMAP] = tmap;
     _objectMap[-1] = obj;
     _isModified = true;
     emit modifiedStatus(_isModified);
}

QJsonArray qtauSession::getTempoMap()
{
    QJsonObject obj;
    if(_objectMap.contains(-1)) obj=_objectMap[-1];
    return obj[TEMPOMAP].toArray();
}

quint64 qtauSession::getNote(QJsonObject note)
{
    foreach(quint64 key, _objectMap.keys())
    {
        if(_objectMap[key]==note) return key;
    }
    return -1;
}

QString qtauSession::undoAction()
{
    switch(events.top()->type())
    {
        case ENoteEvents::add:
            return "add note";
        case ENoteEvents::move:
            return "move note";
        case ENoteEvents::resize:
            return "resize note";
        case ENoteEvents::text:
            return "text change";
    }
    return "";
}

QString qtauSession::redoAction()
{
    switch(futureEvents.top()->type())
    {
        case ENoteEvents::add:
            return "add note";
        case ENoteEvents::move:
            return "move note";
        case ENoteEvents::resize:
            return "resize note";
        case ENoteEvents::text:
            return "text change";
    }
    return "";
}

